package gagak.framework.web.reflect;


import gagak.framework.web.utils.Assert;
import gagak.framework.web.utils.ObjectUtils;
import gagak.framework.web.utils.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;


/*
 * @program: ce
 * @description:
 * @author: houhong
 * @create: 2022-11-23 22:18
 */
public abstract class AnnotationUtils {


    public static final String VALUE = "value";

    private static final Map<AnnotationCacheKey, Annotation> findAnnotationCache =
            new ConcurrentHashMap<AnnotationCacheKey, Annotation>(256);

    private static final Map<AnnotationCacheKey, Boolean> metaPresentCache =
            new ConcurrentHashMap<AnnotationCacheKey, Boolean>(256);

    private static final Map<Class<?>, Set<Method>> annotatedBaseTypeCache =
            new ConcurrentHashMap<Class<?>, Set<Method>>(256);


    private static final Map<Class<?>, ?> annotatedInterfaceCache = annotatedBaseTypeCache;

    private static final Map<Class<? extends Annotation>, Boolean> synthesizableCache =
            new ConcurrentHashMap<Class<? extends Annotation>, Boolean>(256);



    private static final Map<Class<? extends Annotation>, List<Method>> attributeMethodsCache =
            new ConcurrentHashMap<Class<? extends Annotation>, List<Method>>(256);



    private static transient Log logger;


    public static <A extends Annotation> A getAnnotation(Annotation annotation, Class<A> annotationType) {
        if (annotationType.isInstance(annotation)) {
            return synthesizeAnnotation((A) annotation);
        }
        Class<? extends Annotation> annotatedElement = annotation.annotationType();
        try {
            A metaAnn = annotatedElement.getAnnotation(annotationType);
            return (metaAnn != null ? synthesizeAnnotation(metaAnn, annotatedElement) : null);
        } catch (Throwable ex) {
            handleIntrospectionFailure(annotatedElement, ex);
            return null;
        }
    }


    public static <A extends Annotation> A getAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType) {
        try {
            A annotation = annotatedElement.getAnnotation(annotationType);
            if (annotation == null) {
                for (Annotation metaAnn : annotatedElement.getAnnotations()) {
                    annotation = metaAnn.annotationType().getAnnotation(annotationType);
                    if (annotation != null) {
                        break;
                    }
                }
            }
            return (annotation != null ? synthesizeAnnotation(annotation, annotatedElement) : null);
        } catch (Throwable ex) {
            handleIntrospectionFailure(annotatedElement, ex);
            return null;
        }
    }

    public static <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) {
        Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
        return getAnnotation((AnnotatedElement) resolvedMethod, annotationType);
    }


    public static Annotation[] getAnnotations(AnnotatedElement annotatedElement) {
        try {
            return synthesizeAnnotationArray(annotatedElement.getAnnotations(), annotatedElement);
        } catch (Throwable ex) {
            handleIntrospectionFailure(annotatedElement, ex);
            return null;
        }
    }


    public static Annotation[] getAnnotations(Method method) {
        try {
            return synthesizeAnnotationArray(BridgeMethodResolver.findBridgedMethod(method).getAnnotations(), method);
        } catch (Throwable ex) {
            handleIntrospectionFailure(method, ex);
            return null;
        }
    }


    public static <A extends Annotation> Set<A> getRepeatableAnnotations(AnnotatedElement annotatedElement,
                                                                         Class<A> annotationType) {

        return getRepeatableAnnotations(annotatedElement, annotationType, null);
    }

    public static <A extends Annotation> Set<A> getRepeatableAnnotations(AnnotatedElement annotatedElement,
                                                                         Class<A> annotationType,
                                                                         Class<? extends Annotation> containerAnnotationType) {

        Set<A> annotations = getDeclaredRepeatableAnnotations(annotatedElement, annotationType, containerAnnotationType);
        if (!annotations.isEmpty()) {
            return annotations;
        }

        if (annotatedElement instanceof Class) {
            Class<?> superclass = ((Class<?>) annotatedElement).getSuperclass();
            if (superclass != null && superclass != Object.class) {
                return getRepeatableAnnotations(superclass, annotationType, containerAnnotationType);
            }
        }

        return getRepeatableAnnotations(annotatedElement, annotationType, containerAnnotationType, false);
    }


    public static <A extends Annotation> Set<A> getDeclaredRepeatableAnnotations(AnnotatedElement annotatedElement,
                                                                                 Class<A> annotationType) {

        return getDeclaredRepeatableAnnotations(annotatedElement, annotationType, null);
    }


    public static <A extends Annotation> Set<A> getDeclaredRepeatableAnnotations(AnnotatedElement annotatedElement,
                                                                                 Class<A> annotationType, Class<? extends Annotation> containerAnnotationType) {

        return getRepeatableAnnotations(annotatedElement, annotationType, containerAnnotationType, true);
    }


    private static <A extends Annotation> Set<A> getRepeatableAnnotations(AnnotatedElement annotatedElement,
                                                                          Class<A> annotationType, Class<? extends Annotation> containerAnnotationType, boolean declaredMode) {

        try {
            if (annotatedElement instanceof Method) {
                annotatedElement = BridgeMethodResolver.findBridgedMethod((Method) annotatedElement);
            }
            return new AnnotationCollector<A>(annotationType, containerAnnotationType, declaredMode).getResult(annotatedElement);
        } catch (Throwable ex) {
            handleIntrospectionFailure(annotatedElement, ex);
            return Collections.emptySet();
        }
    }


    public static <A extends Annotation> A findAnnotation(
            AnnotatedElement annotatedElement, Class<A> annotationType) {

        if (annotationType == null) {
            return null;
        }


        A ann = findAnnotation(annotatedElement, annotationType, new HashSet<Annotation>());
        return (ann != null ? synthesizeAnnotation(ann, annotatedElement) : null);
    }

    private static <A extends Annotation> A findAnnotation(
            AnnotatedElement annotatedElement, Class<A> annotationType, Set<Annotation> visited) {
        try {
            A annotation = null;
            Annotation[] declaredAnnotations = annotatedElement.getDeclaredAnnotations();
            for (Annotation declaredAnnotation : declaredAnnotations) {
                if (declaredAnnotation.annotationType().isAssignableFrom(annotationType)) {
                    annotation = (A) declaredAnnotation;
                }

            }
            //=annotatedElement.getDeclaredAnnotation(annotationType);
            if (annotation != null) {
                return annotation;
            }
            for (Annotation declaredAnn : annotatedElement.getDeclaredAnnotations()) {
                Class<? extends Annotation> declaredType = declaredAnn.annotationType();
                if (!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) {
                    annotation = findAnnotation((AnnotatedElement) declaredType, annotationType, visited);
                    if (annotation != null) {
                        return annotation;
                    }
                }
            }
        } catch (Throwable ex) {
            handleIntrospectionFailure(annotatedElement, ex);
        }
        return null;
    }


    public static <A extends Annotation> A findAnnotation(Method method, Class<A> annotationType) {
        Assert.notNull(method, "Method must not be null");
        if (annotationType == null) {
            return null;
        }

        AnnotationCacheKey cacheKey = new AnnotationCacheKey(method, annotationType);
        A result = (A) findAnnotationCache.get(cacheKey);

        if (result == null) {
            Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
            result = findAnnotation((AnnotatedElement) resolvedMethod, annotationType);
            if (result == null) {
                result = searchOnInterfaces(method, annotationType, method.getDeclaringClass().getInterfaces());
            }

            Class<?> clazz = method.getDeclaringClass();
            while (result == null) {
                clazz = clazz.getSuperclass();
                if (clazz == null || clazz == Object.class) {
                    break;
                }
                Set<Method> annotatedMethods = getAnnotatedMethodsInBaseType(clazz);
                if (!annotatedMethods.isEmpty()) {
                    for (Method annotatedMethod : annotatedMethods) {
                        if (isOverride(method, annotatedMethod)) {
                            Method resolvedSuperMethod = BridgeMethodResolver.findBridgedMethod(annotatedMethod);
                            result = findAnnotation((AnnotatedElement) resolvedSuperMethod, annotationType);
                            if (result != null) {
                                break;
                            }
                        }
                    }
                }
                if (result == null) {
                    result = searchOnInterfaces(method, annotationType, clazz.getInterfaces());
                }
            }

            if (result != null) {
                result = synthesizeAnnotation(result, method);
                findAnnotationCache.put(cacheKey, result);
            }
        }

        return result;
    }


    private static <A extends Annotation> A searchOnInterfaces(Method method, Class<A> annotationType, Class<?>... ifcs) {
        for (Class<?> ifc : ifcs) {
            Set<Method> annotatedMethods = getAnnotatedMethodsInBaseType(ifc);
            if (!annotatedMethods.isEmpty()) {
                for (Method annotatedMethod : annotatedMethods) {
                    if (isOverride(method, annotatedMethod)) {
                        A annotation = getAnnotation(annotatedMethod, annotationType);
                        if (annotation != null) {
                            return annotation;
                        }
                    }
                }
            }
        }
        return null;
    }


    public static boolean isOverride(Method method, Method candidate) {
        if (!candidate.getName().equals(method.getName()) ||
                candidate.getParameterTypes().length != method.getParameterTypes().length) {
            return false;
        }
        Class<?>[] paramTypes = method.getParameterTypes();
        if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) {
            return true;
        }
        for (int i = 0; i < paramTypes.length; i++) {
            if (paramTypes[i] != ResolvableType.forMethodParameter(candidate, i, method.getDeclaringClass()).resolve()) {
                return false;
            }
        }
        return true;
    }


    public static Set<Method> getAnnotatedMethodsInBaseType(Class<?> baseType) {
        boolean ifcCheck = baseType.isInterface();
        if (ifcCheck && ClassUtils.isJavaLanguageInterface(baseType)) {
            return Collections.emptySet();
        }

        Set<Method> annotatedMethods = annotatedBaseTypeCache.get(baseType);
        if (annotatedMethods != null) {
            return annotatedMethods;
        }
        Method[] methods = (ifcCheck ? baseType.getMethods() : baseType.getDeclaredMethods());
        for (Method baseMethod : methods) {
            try {

                if ((ifcCheck || !Modifier.isPrivate(baseMethod.getModifiers())) &&
                        hasSearchableAnnotations(baseMethod)) {
                    if (annotatedMethods == null) {
                        annotatedMethods = new HashSet<Method>();
                    }
                    annotatedMethods.add(baseMethod);
                }
            } catch (Throwable ex) {
                handleIntrospectionFailure(baseMethod, ex);
            }
        }
        if (annotatedMethods == null) {
            annotatedMethods = Collections.emptySet();
        }
        annotatedBaseTypeCache.put(baseType, annotatedMethods);
        return annotatedMethods;
    }


    private static boolean hasSearchableAnnotations(Method ifcMethod) {
        Annotation[] anns = ifcMethod.getAnnotations();
        if (anns.length == 0) {
            return false;
        }
        for (Annotation ann : anns) {
            Class<?> annType = ann.annotationType();
            if (annType != Deprecated.class) {
                return true;
            }
        }
        return false;
    }


    public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) {
        return findAnnotation(clazz, annotationType, true);
    }

    private static <A extends Annotation> A findAnnotation(
            Class<?> clazz, Class<A> annotationType, boolean synthesize) {

        Assert.notNull(clazz, "Class must not be null");
        if (annotationType == null) {
            return null;
        }

        AnnotationCacheKey cacheKey = new AnnotationCacheKey(clazz, annotationType);
        A result = (A) findAnnotationCache.get(cacheKey);
        if (result == null) {
            result = findAnnotation(clazz, annotationType, new HashSet<Annotation>());
            if (result != null && synthesize) {
                result = synthesizeAnnotation(result, clazz);
                findAnnotationCache.put(cacheKey, result);
            }
        }
        return result;
    }


    private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, Set<Annotation> visited) {
        try {
            A annotation = null;
            Annotation[] declaredAnnotations = clazz.getDeclaredAnnotations();
            for (Annotation declaredAnnotation : declaredAnnotations) {
                if (declaredAnnotation.annotationType().isAssignableFrom(annotationType)) {
                    annotation = (A) declaredAnnotation;
                    break;
                }
            }
            if (annotation != null) {
                return annotation;
            }
            for (Annotation declaredAnn : clazz.getDeclaredAnnotations()) {
                Class<? extends Annotation> declaredType = declaredAnn.annotationType();
                if (!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) {
                    annotation = findAnnotation(declaredType, annotationType, visited);
                    if (annotation != null) {
                        return annotation;
                    }
                }
            }
        } catch (Throwable ex) {
            handleIntrospectionFailure(clazz, ex);
            return null;
        }

        for (Class<?> ifc : clazz.getInterfaces()) {
            A annotation = findAnnotation(ifc, annotationType, visited);
            if (annotation != null) {
                return annotation;
            }
        }

        Class<?> superclass = clazz.getSuperclass();
        if (superclass == null || superclass == Object.class) {
            return null;
        }
        return findAnnotation(superclass, annotationType, visited);
    }


    public static Class<?> findAnnotationDeclaringClass(Class<? extends Annotation> annotationType, Class<?> clazz) {
        if (clazz == null || clazz == Object.class) {
            return null;
        }
        if (isAnnotationDeclaredLocally(annotationType, clazz)) {
            return clazz;
        }
        return findAnnotationDeclaringClass(annotationType, clazz.getSuperclass());
    }


    public static Class<?> findAnnotationDeclaringClassForTypes(
            List<Class<? extends Annotation>> annotationTypes, Class<?> clazz) {

        if (clazz == null || clazz == Object.class) {
            return null;
        }
        for (Class<? extends Annotation> annotationType : annotationTypes) {
            if (isAnnotationDeclaredLocally(annotationType, clazz)) {
                return clazz;
            }
        }
        return findAnnotationDeclaringClassForTypes(annotationTypes, clazz.getSuperclass());
    }


    public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> annotationType, Class<?> clazz) {
        try {
            Annotation resu = null;
            Annotation[] declaredAnnotations = clazz.getDeclaredAnnotations();
            for (Annotation declaredAnnotation : declaredAnnotations) {
                if (declaredAnnotation.annotationType().isAssignableFrom(annotationType)) {
                    resu = declaredAnnotation;
                    break;
                }
            }
            return (resu != null);
        } catch (Throwable ex) {
            handleIntrospectionFailure(clazz, ex);
            return false;
        }
    }


    public static boolean isAnnotationInherited(Class<? extends Annotation> annotationType, Class<?> clazz) {
        return (clazz.isAnnotationPresent(annotationType) && !isAnnotationDeclaredLocally(annotationType, clazz));
    }


    public static boolean isAnnotationMetaPresent(Class<? extends Annotation> annotationType,
                                                  Class<? extends Annotation> metaAnnotationType) {

        Assert.notNull(annotationType, "Annotation type must not be null");
        if (metaAnnotationType == null) {
            return false;
        }

        AnnotationCacheKey cacheKey = new AnnotationCacheKey(annotationType, metaAnnotationType);
        Boolean metaPresent = metaPresentCache.get(cacheKey);
        if (metaPresent != null) {
            return metaPresent;
        }
        metaPresent = Boolean.FALSE;
        if (findAnnotation(annotationType, metaAnnotationType, false) != null) {
            metaPresent = Boolean.TRUE;
        }
        metaPresentCache.put(cacheKey, metaPresent);
        return metaPresent;
    }


    public static boolean isInJavaLangAnnotationPackage(Annotation annotation) {
        return (annotation != null && isInJavaLangAnnotationPackage(annotation.annotationType()));
    }


    public static boolean isInJavaLangAnnotationPackage(Class<? extends Annotation> annotationType) {
        return (annotationType != null && isInJavaLangAnnotationPackage(annotationType.getName()));
    }


    public static boolean isInJavaLangAnnotationPackage(String annotationType) {
        return (annotationType != null && annotationType.startsWith("java.lang.annotation"));
    }


    public static void validateAnnotation(Annotation annotation) {
        for (Method method : getAttributeMethods(annotation.annotationType())) {
            Class<?> returnType = method.getReturnType();
            if (returnType == Class.class || returnType == Class[].class) {
                try {
                    method.invoke(annotation);
                } catch (Throwable ex) {
                    throw new IllegalStateException("Could not obtain annotation attribute value for " + method, ex);
                }
            }
        }
    }


    public static Map<String, Object> getAnnotationAttributes(Annotation annotation) {
        return getAnnotationAttributes(null, annotation);
    }


    public static Map<String, Object> getAnnotationAttributes(
            Annotation annotation, boolean classValuesAsString) {

        return getAnnotationAttributes(annotation, classValuesAsString, false);
    }


    public static AnnotationAttributes getAnnotationAttributes(
            Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

        return getAnnotationAttributes(null, annotation, classValuesAsString, nestedAnnotationsAsMap);
    }

    public static AnnotationAttributes getAnnotationAttributes(
            AnnotatedElement annotatedElement, Annotation annotation) {

        return getAnnotationAttributes(annotatedElement, annotation, false, false);
    }


    public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement,
                                                               Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

        return getAnnotationAttributes(
                (Object) annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap);
    }

    private static AnnotationAttributes getAnnotationAttributes(Object annotatedElement,
                                                                Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

        AnnotationAttributes attributes =
                retrieveAnnotationAttributes(annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap);
        postProcessAnnotationAttributes(annotatedElement, attributes, classValuesAsString, nestedAnnotationsAsMap);
        return attributes;
    }


    public static AnnotationAttributes retrieveAnnotationAttributes(Object annotatedElement, Annotation annotation,
                                                                    boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

        Class<? extends Annotation> annotationType = annotation.annotationType();
        AnnotationAttributes attributes = new AnnotationAttributes(annotationType);

        for (Method method : getAttributeMethods(annotationType)) {
            try {
                Object attributeValue = method.invoke(annotation);
                Object defaultValue = method.getDefaultValue();
                if (defaultValue != null && ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
                    attributeValue = new DefaultValueHolder(defaultValue);
                }
                attributes.put(method.getName(),
                        adaptValue(annotatedElement, attributeValue, classValuesAsString, nestedAnnotationsAsMap));
            } catch (Throwable ex) {
                if (ex instanceof InvocationTargetException) {
                    Throwable targetException = ((InvocationTargetException) ex).getTargetException();
                    rethrowAnnotationConfigurationException(targetException);
                }
                throw new IllegalStateException("Could not obtain annotation attribute value for " + method, ex);
            }
        }

        return attributes;
    }


    public static Object adaptValue(Object annotatedElement, Object value,
                                    boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

        if (classValuesAsString) {
            if (value instanceof Class) {
                return ((Class<?>) value).getName();
            } else if (value instanceof Class[]) {
                Class<?>[] clazzArray = (Class<?>[]) value;
                String[] classNames = new String[clazzArray.length];
                for (int i = 0; i < clazzArray.length; i++) {
                    classNames[i] = clazzArray[i].getName();
                }
                return classNames;
            }
        }

        if (value instanceof Annotation) {
            Annotation annotation = (Annotation) value;
            if (nestedAnnotationsAsMap) {
                return getAnnotationAttributes(annotatedElement, annotation, classValuesAsString, true);
            } else {
                return synthesizeAnnotation(annotation, annotatedElement);
            }
        }

        if (value instanceof Annotation[]) {
            Annotation[] annotations = (Annotation[]) value;
            if (nestedAnnotationsAsMap) {
                AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[annotations.length];
                for (int i = 0; i < annotations.length; i++) {
                    mappedAnnotations[i] =
                            getAnnotationAttributes(annotatedElement, annotations[i], classValuesAsString, true);
                }
                return mappedAnnotations;
            } else {
                return synthesizeAnnotationArray(annotations, annotatedElement);
            }
        }

        // Fallback
        return value;
    }


    public static void registerDefaultValues(AnnotationAttributes attributes) {

        Class<? extends Annotation> annotationType = attributes.annotationType();
        if (annotationType != null && Modifier.isPublic(annotationType.getModifiers())) {
            // Check declared default values of attributes in the annotation type.
            for (Method annotationAttribute : getAttributeMethods(annotationType)) {
                String attributeName = annotationAttribute.getName();
                Object defaultValue = annotationAttribute.getDefaultValue();
                if (defaultValue != null && !attributes.containsKey(attributeName)) {
                    if (defaultValue instanceof Annotation) {
                        defaultValue = getAnnotationAttributes((Annotation) defaultValue, false, true);
                    } else if (defaultValue instanceof Annotation[]) {
                        Annotation[] realAnnotations = (Annotation[]) defaultValue;
                        AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length];
                        for (int i = 0; i < realAnnotations.length; i++) {
                            mappedAnnotations[i] = getAnnotationAttributes(realAnnotations[i], false, true);
                        }
                        defaultValue = mappedAnnotations;
                    }
                    attributes.put(attributeName, new DefaultValueHolder(defaultValue));
                }
            }
        }
    }


    public static void postProcessAnnotationAttributes(Object annotatedElement,
                                                       AnnotationAttributes attributes, boolean classValuesAsString) {

        postProcessAnnotationAttributes(annotatedElement, attributes, classValuesAsString, false);
    }


    public static void postProcessAnnotationAttributes(Object annotatedElement,
                                                       AnnotationAttributes attributes, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

        if (attributes == null) {
            return;
        }

        Class<? extends Annotation> annotationType = attributes.annotationType();


        Set<String> valuesAlreadyReplaced = new HashSet<String>();

        if (!attributes.validated) {
            attributes.validated = true;
        }


        for (String attributeName : attributes.keySet()) {
            if (valuesAlreadyReplaced.contains(attributeName)) {
                continue;
            }
            Object value = attributes.get(attributeName);
            if (value instanceof DefaultValueHolder) {
                value = ((DefaultValueHolder) value).defaultValue;
                attributes.put(attributeName,
                        adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
            }
        }
    }


    public static Object getValue(Annotation annotation) {
        return getValue(annotation, VALUE);
    }


    public static Object getValue(Annotation annotation, String attributeName) {
        if (annotation == null || !StringUtils.hasText(attributeName)) {
            return null;
        }
        try {
            Method method = annotation.annotationType().getDeclaredMethod(attributeName);
            ReflectionUtils.makeAccessible(method);
            return method.invoke(annotation);
        } catch (NoSuchMethodException ex) {
            return null;
        } catch (InvocationTargetException ex) {
            rethrowAnnotationConfigurationException(ex.getTargetException());
            throw new IllegalStateException(
                    "Could not obtain value for annotation attribute '" + attributeName + "' in " + annotation, ex);
        } catch (Throwable ex) {
            handleIntrospectionFailure(annotation.getClass(), ex);
            return null;
        }
    }


    public static Object getDefaultValue(Annotation annotation) {
        return getDefaultValue(annotation, VALUE);
    }


    public static Object getDefaultValue(Annotation annotation, String attributeName) {
        return (annotation != null ? getDefaultValue(annotation.annotationType(), attributeName) : null);
    }


    public static Object getDefaultValue(Class<? extends Annotation> annotationType) {
        return getDefaultValue(annotationType, VALUE);
    }


    public static Object getDefaultValue(
            Class<? extends Annotation> annotationType, String attributeName) {

        if (annotationType == null || !StringUtils.hasText(attributeName)) {
            return null;
        }
        try {
            return annotationType.getDeclaredMethod(attributeName).getDefaultValue();
        } catch (Throwable ex) {
            handleIntrospectionFailure(annotationType, ex);
            return null;
        }
    }


    static <A extends Annotation> A synthesizeAnnotation(A annotation) {
        return synthesizeAnnotation(annotation, null);
    }


    public static <A extends Annotation> A synthesizeAnnotation(
            A annotation, AnnotatedElement annotatedElement) {

        return synthesizeAnnotation(annotation, (Object) annotatedElement);
    }

    @SuppressWarnings("unchecked")
    static <A extends Annotation> A synthesizeAnnotation(A annotation, Object annotatedElement) {
        if (annotation instanceof SynthesizedAnnotation) {
            return annotation;
        }

        Class<? extends Annotation> annotationType = annotation.annotationType();
        if (!isSynthesizable(annotationType)) {
            return annotation;
        }




        Class<?>[] exposedInterfaces = new Class<?>[]{annotationType, SynthesizedAnnotation.class};
        return (A) Proxy.newProxyInstance(annotation.getClass().getClassLoader(), exposedInterfaces, null);
    }


    public static <A extends Annotation> A synthesizeAnnotation(Map<String, Object> attributes,
                                                                Class<A> annotationType, AnnotatedElement annotatedElement) {


        Class<?>[] exposedInterfaces = (canExposeSynthesizedMarker(annotationType) ?
                new Class<?>[]{annotationType, SynthesizedAnnotation.class} : new Class<?>[]{annotationType});
        return (A) Proxy.newProxyInstance(annotationType.getClassLoader(), exposedInterfaces, null);
    }


    public static <A extends Annotation> A synthesizeAnnotation(Class<A> annotationType) {
        return synthesizeAnnotation(new HashMap<String, Object>(), annotationType, null);
    }


    static Annotation[] synthesizeAnnotationArray(Annotation[] annotations, Object annotatedElement) {
        Annotation[] synthesized = (Annotation[]) Array.newInstance(
                annotations.getClass().getComponentType(), annotations.length);
        for (int i = 0; i < annotations.length; i++) {
            synthesized[i] = synthesizeAnnotation(annotations[i], annotatedElement);
        }
        return synthesized;
    }


    static <A extends Annotation> A[] synthesizeAnnotationArray(
            Map<String, Object>[] maps, Class<A> annotationType) {

        if (maps == null) {
            return null;
        }

        A[] synthesized = (A[]) Array.newInstance(annotationType, maps.length);
        for (int i = 0; i < maps.length; i++) {
            synthesized[i] = synthesizeAnnotation(maps[i], annotationType, null);
        }
        return synthesized;
    }





    private static boolean canExposeSynthesizedMarker(Class<? extends Annotation> annotationType) {
        try {
            return (Class.forName(SynthesizedAnnotation.class.getName(), false, annotationType.getClassLoader()) ==
                    SynthesizedAnnotation.class);
        } catch (ClassNotFoundException ex) {
            return false;
        }
    }


    private static boolean isSynthesizable(Class<? extends Annotation> annotationType) {
        if (isInJavaLangAnnotationPackage(annotationType)) {
            return false;
        }

        Boolean synthesizable = synthesizableCache.get(annotationType);
        if (synthesizable != null) {
            return synthesizable;
        }

        synthesizable = Boolean.FALSE;
        for (Method attribute : getAttributeMethods(annotationType)) {

            Class<?> returnType = attribute.getReturnType();
            if (Annotation[].class.isAssignableFrom(returnType)) {
                Class<? extends Annotation> nestedAnnotationType =
                        (Class<? extends Annotation>) returnType.getComponentType();
                if (isSynthesizable(nestedAnnotationType)) {
                    synthesizable = Boolean.TRUE;
                    break;
                }
            } else if (Annotation.class.isAssignableFrom(returnType)) {
                Class<? extends Annotation> nestedAnnotationType = (Class<? extends Annotation>) returnType;
                if (isSynthesizable(nestedAnnotationType)) {
                    synthesizable = Boolean.TRUE;
                    break;
                }
            }
        }

        synthesizableCache.put(annotationType, synthesizable);
        return synthesizable;
    }






    public static List<Method> getAttributeMethods(Class<? extends Annotation> annotationType) {
        List<Method> methods = attributeMethodsCache.get(annotationType);
        if (methods != null) {
            return methods;
        }

        methods = new ArrayList<Method>();
        for (Method method : annotationType.getDeclaredMethods()) {
            if (isAttributeMethod(method)) {
                ReflectionUtils.makeAccessible(method);
                methods.add(method);
            }
        }

        attributeMethodsCache.put(annotationType, methods);
        return methods;
    }

    public static Annotation getAnnotation(AnnotatedElement element, String annotationName) {
        for (Annotation annotation : element.getAnnotations()) {
            if (annotation.annotationType().getName().equals(annotationName)) {
                return annotation;
            }
        }
        return null;
    }


    static boolean isAttributeMethod(Method method) {
        return (method != null && method.getParameterTypes().length == 0 && method.getReturnType() != void.class);
    }


    static boolean isAnnotationTypeMethod(Method method) {
        return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0);
    }


    public static Class<? extends Annotation> resolveContainerAnnotationType(Class<? extends Annotation> annotationType) {

        return  null;
    }


    public static void rethrowAnnotationConfigurationException(Throwable ex) {

    }


    public static void handleIntrospectionFailure(AnnotatedElement element, Throwable ex) {
        rethrowAnnotationConfigurationException(ex);

        Log loggerToUse = logger;
        if (loggerToUse == null) {
            loggerToUse = LogFactory.getLog(AnnotationUtils.class);
            logger = loggerToUse;
        }
        if (element instanceof Class && Annotation.class.isAssignableFrom((Class<?>) element)) {
            // Meta-annotation or (default) value lookup on an annotation type
            if (loggerToUse.isDebugEnabled()) {
                loggerToUse.debug("Failed to meta-introspect annotation " + element + ": " + ex);
            }
        } else {
            // Direct annotation lookup on regular Class, Method, Field
            if (loggerToUse.isInfoEnabled()) {
                loggerToUse.info("Failed to introspect annotations on " + element + ": " + ex);
            }
        }
    }


    public static void clearCache() {
        findAnnotationCache.clear();
        metaPresentCache.clear();
        annotatedBaseTypeCache.clear();
        synthesizableCache.clear();
        attributeMethodsCache.clear();
    }


    private static final class AnnotationCacheKey implements Comparable<AnnotationCacheKey> {

        private final AnnotatedElement element;

        private final Class<? extends Annotation> annotationType;

        public AnnotationCacheKey(AnnotatedElement element, Class<? extends Annotation> annotationType) {
            this.element = element;
            this.annotationType = annotationType;
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof AnnotationCacheKey)) {
                return false;
            }
            AnnotationCacheKey otherKey = (AnnotationCacheKey) other;
            return (this.element.equals(otherKey.element) && this.annotationType.equals(otherKey.annotationType));
        }

        @Override
        public int hashCode() {
            return (this.element.hashCode() * 29 + this.annotationType.hashCode());
        }

        @Override
        public String toString() {
            return "@" + this.annotationType + " on " + this.element;
        }

        @Override
        public int compareTo(AnnotationCacheKey other) {
            int result = this.element.toString().compareTo(other.element.toString());
            if (result == 0) {
                result = this.annotationType.getName().compareTo(other.annotationType.getName());
            }
            return result;
        }
    }


    private static class AnnotationCollector<A extends Annotation> {

        private final Class<A> annotationType;


        private final Class<? extends Annotation> containerAnnotationType;

        private final boolean declaredMode;

        private final Set<AnnotatedElement> visited = new HashSet<AnnotatedElement>();

        private final Set<A> result = new LinkedHashSet<A>();

        AnnotationCollector(Class<A> annotationType,
                            Class<? extends Annotation> containerAnnotationType, boolean declaredMode) {

            this.annotationType = annotationType;
            this.containerAnnotationType = (containerAnnotationType != null ? containerAnnotationType :
                    resolveContainerAnnotationType(annotationType));
            this.declaredMode = declaredMode;
        }

        Set<A> getResult(AnnotatedElement element) {
            process(element);
            return Collections.unmodifiableSet(this.result);
        }


        private void process(AnnotatedElement element) {
            if (this.visited.add(element)) {
                try {
                    Annotation[] annotations = (this.declaredMode ? element.getDeclaredAnnotations() : element.getAnnotations());
                    for (Annotation ann : annotations) {
                        Class<? extends Annotation> currentAnnotationType = ann.annotationType();
                        if (ObjectUtils.nullSafeEquals(this.annotationType, currentAnnotationType)) {
                            this.result.add(synthesizeAnnotation((A) ann, element));
                        } else if (ObjectUtils.nullSafeEquals(this.containerAnnotationType, currentAnnotationType)) {
                            this.result.addAll(getValue(element, ann));
                        } else if (!isInJavaLangAnnotationPackage(currentAnnotationType)) {
                            process(currentAnnotationType);
                        }
                    }
                } catch (Throwable ex) {
                    handleIntrospectionFailure(element, ex);
                }
            }
        }

        private List<A> getValue(AnnotatedElement element, Annotation annotation) {
            try {
                List<A> synthesizedAnnotations = new ArrayList<A>();
                A[] value = (A[]) AnnotationUtils.getValue(annotation);
                if (value != null) {
                    for (A anno : value) {
                        synthesizedAnnotations.add(synthesizeAnnotation(anno, element));
                    }
                }
                return synthesizedAnnotations;
            } catch (Throwable ex) {
                handleIntrospectionFailure(element, ex);
            }

            return Collections.emptyList();
        }
    }





    private static class DefaultValueHolder {

        final Object defaultValue;

        public DefaultValueHolder(Object defaultValue) {
            this.defaultValue = defaultValue;
        }
    }

}
